<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
    </head>

    <body>
        <div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox,emby-linkbutton">
            <div data-role="content">
                <style>
                    summary {
                        cursor: pointer;
                        padding: 10px;
                        width: inherit;
                        margin: auto;
                        border: none;
                        text-align: center;
                        font-size: 1em;
                        outline: 2px solid rgba(155, 155, 155, 0.5);
                    }
                    h3.checkboxListLabel {
                        font-size: 1em;
                        margin-bottom: 4px;
                    }
                    /* Disabled state styling */
                    .disabled-block {
                        opacity: .6;
                    }
                    .disabled-block input,
                    .disabled-block select,
                    .disabled-block button {
                        pointer-events: none;
                    }
                    .disabled-block .inputLabel,
                    .disabled-block label.emby-checkbox-label span {
                        font-style: italic;
                    }
                </style>
                <div class="content-primary">
                    <form id="FingerprintConfigForm">
                        <fieldset class="verticalSection-extrabottompadding">
                            <legend>Analysis</legend>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="AutoDetectIntros" type="checkbox" is="emby-checkbox" />
                                    <span>Automatically Analyze New Media</span>
                                </label>

                                <div class="fieldDescription">
                                    If enabled, new media will be automatically analyzed for skippable segments when added to the library
                                    <br />
                                    <br />
                                    Note: To configure the scheduled task, see <a is="emby-linkbutton" class="button-link" href="#/dashboard/tasks">scheduled tasks</a>.
                                </div>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="UpdateMediaSegments" type="checkbox" is="emby-checkbox" />
                                    <span>Update Missing Segments During Scan</span>
                                </label>

                                <div class="fieldDescription">
                                    Enable this option to update media segments for any uncached media during a library scan. <br />
                                    This includes recently added, modified, or previously skipped (but not ignored) files.<br />
                                    <b>Warning:</b> This should be disabled if you're using media segment providers other than Intro Skipper.
                                </div>
                            </div>

                            <div class="inputContainer" id="excludedSeries">
                                <label class="inputLabel inputLabelUnfocused" for="ExcludeSeries"> Exclude series </label>
                                <input id="ExcludeSeries" type="text" is="emby-input" />
                                <div class="fieldDescription">Exclude series from analysis. Enter a comma-separated list of series names to exclude.</div>
                            </div>

                            <div class="checkboxContainer" style="display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center;">
                                <span style="font-weight: 600; margin-right: 1em; white-space: nowrap;">Analyze for:</span>
                                <label class="emby-checkbox-label">
                                    <input id="ScanIntroduction" type="checkbox" is="emby-checkbox" />
                                    <span>Introductions</span>
                                </label>
                                <label class="emby-checkbox-label">
                                    <input id="ScanCredits" type="checkbox" is="emby-checkbox" />
                                    <span>Credits</span>
                                </label>
                                <label class="emby-checkbox-label">
                                    <input id="ScanRecap" type="checkbox" is="emby-checkbox" />
                                    <span>Recaps</span>
                                </label>
                                <label class="emby-checkbox-label">
                                    <input id="ScanPreview" type="checkbox" is="emby-checkbox" />
                                    <span>Previews</span>
                                </label>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
                                    <span>Analyze Season 0 (Specials / Extras)</span>
                                </label>

                                <div class="fieldDescription">Note: Shows containing both a specials and extra folder will identify extras as season 0 and ignore specials, regardless of this setting.</div>
                            </div>

                            <div id="divSkipbuttonHideDelay" class="inputContainer">
                                <label class="emby-checkbox-label">
                                    <input id="UseFileTransformationPlugin" type="checkbox" is="emby-checkbox" />
                                    <span>Use File Transformation Plugin to patch the web interface</span>
                                </label>
                                <div id="divSkipbuttonHideDelayContent">
                                    <label class="inputLabel inputLabelUnfocused" for="SkipbuttonHideDelay"> Skip button hide delay (in seconds) </label>
                                    <input id="SkipbuttonHideDelay" type="number" is="emby-input" min="0" max="1000"/>
                                    <div class="fieldDescription">
                                        <span id="skipButtonDescription">Time in seconds before the skip button automatically hides. Set to 0 for persistent skip button (never hides).</span>
                                    </div>
                                </div>
                                <br />
                                <span style="font-size: 0.85em; color: #ffa500; display: inline-block; margin-top: 4px;">Note: This setting only affects clients using the web client player (web browsers, Android when using the web player, LG webOS, etc). If changes don't appear, refresh the page or clear the browser cache.</span>
                                <div id="fileTransformationWarning" style="display: none; color: orange; margin-top: 8px;">
                                    <strong>File Transformation Plugin Required</strong><br />
                                    This feature requires the File Transformation plugin to work.
                                    <a href="https://github.com/IAmParadox27/jellyfin-plugin-file-transformation" target="_blank" style="color: #2196f3;">
                                        Install it here
                                    </a>
                                </div>
                            </div>

                            <details id="intro_reqs">
                                <summary>Modify Analysis Parameters</summary>

                                <br />
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="PreferChromaprint" type="checkbox" is="emby-checkbox" />
                                        <span>Prefer Chromaprint Analysis</span>
                                    </label>
                                    <div class="fieldDescription">Only use chromaprint for analysis, unless it is not available. Setting an analysis mode in the advanced options will override this setting.</div>
                                </div>

                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="FullLengthChapters" type="checkbox" is="emby-checkbox" />
                                        <span>Ignore duration limits for chapters</span>
                                    </label>
                                    <div class="fieldDescription">Allow segments to extend to the end of a chapter when the marker exceeds other user settings, such as percentage or duration.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AnalysisPercent"> Percent of media to analyze </label>
                                    <input id="AnalysisPercent" type="number" is="emby-input" min="1" max="90" />
                                    <div class="fieldDescription">Analysis will be limited to this percentage of each item's runtime. For example, a value of 25 (the default) will limit analysis to the first quarter of each item.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AnalysisLengthLimit"> Maximum runtime to analyze (in minutes) </label>
                                    <input id="AnalysisLengthLimit" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Analysis will be limited to this amount of each item's runtime. For example, a value of 10 (the default) will limit analysis to the first 10 minutes of each item.</div>
                                </div>

                                <p>The amount of each item's content that will be analyzed is determined using the percentage and maximum runtime. The minimum of (duration * percent, maximum runtime) is the amount that will be analyzed.</p>
                                <p>If the percentage or maximum runtime settings are modified, the cached fingerprints and timestamps for each series, season, or movie you want to analyze with the modified settings <b>will have to be recreated</b>.</p>
                                <p>Increasing either of the above settings will cause episode analysis to take much longer.</p>
                                <br />

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MinimumIntroDuration"> Minimum introduction duration (in seconds) </label>
                                    <input id="MinimumIntroDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments or similar sounding audio which is shorter than this duration will not be considered an introduction.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MaximumIntroDuration"> Maximum introduction duration (in seconds) </label>
                                    <input id="MaximumIntroDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments or similar sounding audio which is longer than this duration will not be considered an introduction.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MinimumCreditsDuration"> Minimum credits duration (in seconds) </label>
                                    <input id="MinimumCreditsDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments or similar sounding audio which is shorter than this duration will not be considered credits.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MaximumCreditsDuration"> Maximum credits duration (in seconds) </label>
                                    <input id="MaximumCreditsDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments or similar sounding audio which is longer than this duration will not be considered credits.</div>
                                </div>

                                <div class="inputContainer" id="movieCreditsDuration">
                                    <label class="inputLabel inputLabelUnfocused" for="MaximumMovieCreditsDuration"> Maximum movie credits duration (in seconds) </label>
                                    <input id="MaximumMovieCreditsDuration" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Segments longer than this duration will not be considered movie credits.</div>
                                </div>

                                <div id="RecapPreviewDurations">
                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="MinimumRecapDuration"> Minimum recap duration (in seconds) </label>
                                        <input id="MinimumRecapDuration" type="number" is="emby-input" min="1" />
                                        <div class="fieldDescription">Segments which are shorter than this duration will not be considered a recap.</div>
                                    </div>

                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="MaximumRecapDuration"> Maximum recap duration (in seconds) </label>
                                        <input id="MaximumRecapDuration" type="number" is="emby-input" min="1" />
                                        <div class="fieldDescription">Segments which are longer than this duration will not be considered a recap.</div>
                                    </div>

                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="MinimumPreviewDuration"> Minimum preview duration (in seconds) </label>
                                        <input id="MinimumPreviewDuration" type="number" is="emby-input" min="1" />
                                        <div class="fieldDescription">Segments which are shorter than this duration will not be considered a preview.</div>
                                    </div>

                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="MaximumPreviewDuration"> Maximum preview duration (in seconds) </label>
                                        <input id="MaximumPreviewDuration" type="number" is="emby-input" min="1" />
                                        <div class="fieldDescription">Segments which are longer than this duration will not be considered a preview.</div>
                                    </div>
                                </div>
                                <br />
                            </details>

                            <details id="adjustment">
                                <summary>Detection Adjustment Options</summary>

                                <br />
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="AdjustIntroBasedOnSilence" type="checkbox" is="emby-checkbox" />
                                        <span>Enable silence detection</span>
                                    </label>

                                    <div class="fieldDescription">When enabled, segment endpoints will be adjusted to the nearest silence point.</div>
                                </div>
                                <div id="silenceSettings">
                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMaximumNoise"> Noise tolerance </label>
                                        <input id="SilenceDetectionMaximumNoise" type="number" is="emby-input" min="-90" max="0" />
                                        <div class="fieldDescription">Noise tolerance in negative decibels.</div>
                                    </div>

                                    <div class="inputContainer">
                                        <label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMinimumDuration"> Minimum silence duration </label>
                                        <input id="SilenceDetectionMinimumDuration" type="number" is="emby-input" min="0" step="0.01" />
                                        <div class="fieldDescription">Minimum silence duration in seconds before adjusting introduction end time.</div>
                                    </div>
                                    <br />
                                </div>
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="SnapToKeyframe" type="checkbox" is="emby-checkbox" />
                                        <span>Enable keyframe snapping</span>
                                    </label>

                                    <div class="fieldDescription">When enabled, segment endpoints will be adjusted to the nearest video keyframe for smoother seek transitions during skipping.</div>
                                </div>
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="AdjustIntroBasedOnChapters" type="checkbox" is="emby-checkbox" />
                                        <span>Enable chapter snapping</span>
                                    </label>

                                    <div class="fieldDescription">When enabled, segment start and end times will be adjusted to the nearest chapter boundary.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AdjustWindowInward">Adjustment window (inward)</label>
                                    <input id="AdjustWindowInward" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">Maximum number of seconds to search toward a segment's interior for adjustment points (like chapter boundaries, silence, or keyframes). Used to tighten segment boundaries.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AdjustWindowOutward">Adjustment window (outward)</label>
                                    <input id="AdjustWindowOutward" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">Maximum number of seconds to search away from a segment for adjustment points (like chapter boundaries, silence, or keyframes). Used to expand segment boundaries.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="EndSnapThreshold"> Snap to episode start/end threshold </label>
                                    <input id="EndSnapThreshold" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">If a segment's start or end is within this many seconds of the episode's start or end, it will be automatically adjusted (snapped) to match the episode boundary. Set to 0 to disable snapping.</div>
                                </div>

                                <fieldset>
                                  <legend>Segment Offset Adjustment</legend>
                                  <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="IntroStartOffset">Intro Start Offset (seconds)</label>
                                    <input id="IntroStartOffset" is="emby-input" type="number" min="0" value="0" step="0.5" />
                                    <div class="fieldDescription">
                                      Default: 0. Example: If set to 3, the first 3 seconds of the intro will play before skipping.
                                    </div>
                                  </div>
                                  <div class="inputContainer">
                                    <label for="IntroEndOffset">Intro End Offset (seconds)</label>
                                    <input id="IntroEndOffset" is="emby-input" type="number" min="0" value="0" step="0.5" />
                                    <div class="fieldDescription">
                                      Default: 0. Example: If set to 3, playback will resume 3 seconds before the end of the intro.
                                    </div>
                                  </div>
                                </fieldset>
                                <br />
                            </details>

                            <details id="blackframe">
                                <summary>Black Frame Detection Options</summary>

                                <br />
                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="UseAlternativeBlackFrameAnalyzer" type="checkbox" is="emby-checkbox" />
                                        <span>Use alternative black frame analyzer (experimental)</span>
                                    </label>

                                    <div class="fieldDescription">If enabled, the alternative black frame analyzer will be used. This analyzer is experimental and may not work as expected.</div>
                                </div>

                                <div class="checkboxContainer checkboxContainer-withDescription" id="ChapterMarkersBlackFrameSetting">
                                    <label class="emby-checkbox-label">
                                        <input id="UseChapterMarkersBlackFrame" type="checkbox" is="emby-checkbox" />
                                        <span>Use chapter markers for credits detection</span>
                                    </label>

                                    <div class="fieldDescription">If enabled, chapter markers will be used to identify credits segments. Tries to detect credits by looking for a black frames close to chapter markers.</div>
                                    <br />
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="BlackFrameMinimumPercentage"> Minimum percentage of black pixels </label>
                                    <input id="BlackFrameMinimumPercentage" type="number" is="emby-input" min="0" max="100" />
                                    <div class="fieldDescription">Minimum percentage of black pixels in a frame before it is considered a black frame. Defaults to 85.</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="BlackFrameThreshold"> Black frame threshold </label>
                                    <input id="BlackFrameThreshold" type="number" is="emby-input" min="16" max="255" />
                                    <div class="fieldDescription">The threshold below which a pixel value is considered black. Defaults to 32.</div>
                                </div>
                            </details>

                            <details id="chapters">
                                <summary>Chapter Detection Options</summary>

                                <br />
                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerIntroductionPattern"> Introductions </label>
                                    <input id="ChapterAnalyzerIntroductionPattern" type="text" placeholder="(^|\s)(Intro|Introduction|OP|Opening)(?!\sEnd)(\s|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect introduction chapters. <br />Default: <code>(^|\s)(Intro|Introduction|OP|Opening)(?!\sEnd)(\s|$)</code></div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerEndCreditsPattern"> Credits </label>
                                    <input id="ChapterAnalyzerEndCreditsPattern" type="text" placeholder="(^|\s)(Credits?|ED|Ending|Outro)(?!\sEnd)(\s|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect credits chapters. <br />Default: <code>(^|\s)(Credits?|ED|Ending|Outro)(?!\sEnd)(\s|$)</code></div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerPreviewPattern"> Preview </label>
                                    <input id="ChapterAnalyzerPreviewPattern" type="text" placeholder="(^|\s)(Preview|PV|Sneak\s?Peek|Coming\s?(Up|Soon)|Next\s+(time|on|episode)|Extra|Teaser|Trailer)(?!\sEnd)(\s|:|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect preview chapters. <br />Default: <code>(^|\s)(Preview|PV|Sneak\s?Peek|Coming\s?(Up|Soon)|Next\s+(time|on|episode)|Extra|Teaser|Trailer)(?!\sEnd)(\s|:|$)</code></div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ChapterAnalyzerRecapPattern"> Recaps </label>
                                    <input id="ChapterAnalyzerRecapPattern" type="text" placeholder="(^|\s)(Re?cap|Sum{1,2}ary|Prev(ious(ly)?)?|(Last|Earlier)(\s\w+)?|Catch[ -]up)(?!\sEnd)(\s|:|$)" is="emby-input" />
                                    <div class="fieldDescription">Enter a regular expression to detect recap chapters. <br />Default: <code>(^|\s)(Re?cap|Sum{1,2}ary|Prev(ious(ly)?)?|(Last|Earlier)(\s\w+)?|Catch[ -]up)(?!\sEnd)(\s|:|$)</code></div>
                                </div>
                            </details>

                            <details id="detection">
                                <summary>FFmpeg Process / Advanced</summary>

                                <br />
                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="MaxParallelism"> Maximum degree of parallelism </label>
                                    <input id="MaxParallelism" type="number" is="emby-input" min="1" />
                                    <div class="fieldDescription">Maximum number of simultaneous async episode analysis operations.</div>
                                </div>

                                <div class="selectContainer">
                                    <label class="selectLabel" for="ProcessPriority">FFmpeg Priority</label>
                                    <select is="emby-select" id="ProcessPriority" class="emby-select-withcolor emby-select">
                                        <option value="Idle">Idle</option>

                                        <option value="BelowNormal">Below Normal</option>

                                        <option value="Normal">Normal</option>

                                        <option value="AboveNormal">Above Normal</option>

                                        <option value="High">High</option>

                                        <option value="RealTime">Highest</option>
                                    </select>

                                    <div class="fieldDescription">Sets the relative priority of the analysis FFmpeg process to other parallel operations (i.e. transcoding, chapter detection, etc).</div>
                                </div>

                                <div class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="ProcessThreads"> FFmpeg Threads </label>
                                    <input id="ProcessThreads" type="number" is="emby-input" min="0" max="16" />
                                    <div class="fieldDescription">
                                        Number of simultaneous processes to use for FFmpeg operations.
                                        <br />
                                        This value is most often defined as 1 thread per CPU core, but setting a value of 0 (default) will use the maximum threads available.
                                    </div>
                                </div>

                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="CacheFingerprints" type="checkbox" is="emby-checkbox" />
                                        <span>Cache analysis fingerprints</span>
                                    </label>

                                    <div class="fieldDescription">
                                        <b>Debug use only!</b> If checked, fingerprints will be saved on the filesystem to improve analysis speed.
                                        <br />
                                        <b>WARNING: Disabling the cache will cause all fingerprints to be recreated and not saved during analysis.</b>
                                        <br />
                                    </div>
                                </div>
                            </details>

                            <p>
                                <b style="color: orange">Changing analysis or detection requires regenerating media segments before changes take effect.</b>
                                <br />
                                Per the jellyfin MediaSegments API, records must be updated individually and may be slow to regenerate.
                            </p>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="RebuildMediaSegments" type="checkbox" is="emby-checkbox" />
                                    <span>Regenerate All Media Segments on Next Run</span>
                                </label>

                                <div class="fieldDescription">When enabled, this option will <b>overwrite all existing media segments</b> with your current Intro Skipper timestamps during the next analysis. Upon completion, this option will be automatically disabled.</div>
                            </div>

                            <p align="center" style="font-size: 0.75em">EDL file generation has been removed. Please use the <a href="https://github.com/intro-skipper/jellyfin-plugin-edl">EDL plugin</a>.</p>
                        </fieldset>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="PluginSkip" type="checkbox" is="emby-checkbox" />
                                <span>Enable <b style="color: red">Server-side</b> Auto Skip</span>
                            </label>
                        </div>

                        <div id="ServerSkipSettings" style="display: none">
                            <fieldset class="verticalSection-extrabottompadding">
                                <legend>Playback</legend>

                                <div class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="AutoSkip" type="checkbox" is="emby-checkbox" />
                                        <span>Automatically Skip for All Clients</span>
                                    </label>
                                </div>

                                <div class="AutoSkipClientListContainer">
                                    <div class="AutoSkipClientList">
                                        <h3 class="checkboxListLabel">Limit auto skip to the following clients</h3>
                                        <div class="checkboxList paperList" style="padding: 0.5em 1em" id="autoSkipCheckboxes"></div>
                                    </div>
                                    <label class="inputLabel" for="ClientList"></label>
                                    <input id="ClientList" type="hidden" is="emby-input" />
                                </div>

                                <div class="AutoSkipTypeListContainer">
                                    <div class="AutoSkipTypeList">
                                        <h3 class="checkboxListLabel">Auto skip the following types</h3>
                                        <div class="checkboxList paperList" style="padding: 0.5em 1em" id="autoSkipTypeCheckboxes"></div>
                                    </div>
                                    <label class="inputLabel" for="TypeList"></label>
                                    <input id="TypeList" type="hidden" is="emby-input" />
                                </div>

                                <div id="divSkipFirstEpisode" class="checkboxContainer checkboxContainer-withDescription">
                                    <label class="emby-checkbox-label">
                                        <input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
                                        <span>Play Segments for First Episode of a Season</span>
                                    </label>

                                    <div class="fieldDescription">If checked, auto skip will play the segments of the first episode in a season.</div>
                                </div>

                                <div id="divAutoSkipDelay" class="inputContainer">
                                    <label class="inputLabel inputLabelUnfocused" for="AutoSkipDelay"> Auto skip delay (in seconds) </label>
                                    <input id="AutoSkipDelay" type="number" is="emby-input" min="0" />
                                    <div class="fieldDescription">Seconds at the start of a segment that should be played before skipping. Defaults to 0.</div>
                                </div>

                                <div id="divAutoSkipNotificationText" class="inputContainer">
                                    <label class="inputLabel" for="AutoSkipNotificationText"> Auto skip notification message </label>
                                    <input id="AutoSkipNotificationText" type="text" is="emby-input" />
                                    <div class="fieldDescription">Message shown after automatically skipping a segment. Leave blank to disable notification. <br />Available variables: %segmenttype, %start, %end, %duration</div>
                                    <p>
                                        <b style="color: orange">This setting does not apply to Media Segment Actions in Jellyfin 10.10 and compatible clients.</b>
                                    </p>
                                </div>
                            </fieldset>
                        </div>

                        <div>
                            <button is="emby-button" type="submit" class="raised button-submit block emby-button">
                                <span>Save</span>
                            </button>
                        </div>
                        <br />

                        <fieldset class="verticalSection-extrabottompadding">
                            <legend>Advanced</legend>

                            <details id="visualizer">
                                <summary>Edit Timestamps & Fingerprints</summary>

                                <br />
                                <label class="inputLabel" for="troubleshooterShow">Select TV series / movie to manage</label>
                                <select is="emby-select" id="troubleshooterShow" class="emby-select-withcolor emby-select"></select>
                                <div id="seasonSelection" style="display: none">
                                    <p>
                                        <label class="inputLabel" for="troubleshooterSeason">Select season to manage</label>
                                        <select is="emby-select" id="troubleshooterSeason" class="emby-select-withcolor emby-select"></select>
                                    </p>
                                </div>

                                <div id="analyzerActionsSection" style="display: none">
                                    <br />
                                    <h3 style="margin: 0">Analyzer actions</h3>
                                    <p style="margin: 0">
                                        Choose how segments should be analyzed for this season.<br />
                                        <i>
                                            Default uses all available detection methods (Chromaprint, Chapter, and BlackFrame for credits).<br />
                                            Select specific methods to limit analysis, or None to skip detection entirely.
                                        </i>
                                    </p>
                                    <br />

                                    <div id="analyzerActionsContainer" style="display: flex; align-items: center; gap: 1.5em">
                                        <label for="actionRecap" style="width: 23%">
                                            <span>Recap analysis</span>
                                            <select is="emby-select" id="actionRecap" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionIntro" style="width: 23%">
                                            <span>Introduction analysis</span>
                                            <select is="emby-select" id="actionIntro" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="Chromaprint">Chromaprint</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionCredits" style="width: 23%">
                                            <span>Credits (Outro) analysis</span>
                                            <select is="emby-select" id="actionCredits" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="Chromaprint">Chromaprint</option>
                                                <option value="BlackFrame">BlackFrame</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                        <label for="actionPreview" style="width: 23%">
                                            <span>Preview analysis</span>
                                            <select is="emby-select" id="actionPreview" class="emby-select-withcolor emby-select">
                                                <option value="Default">Default</option>
                                                <option value="Chapter">Chapter</option>
                                                <option value="None">None</option>
                                            </select>
                                        </label>
                                    </div>
                                    <p>
                                        <button is="emby-button" id="saveAnalyzerActions" class="raised button-submit block emby-button" style="display: none">Apply Changes</button>
                                    </p>
                                    <br />
                                </div>

                                <div id="episodeSelection" style="display: none">
                                    <p>
                                        <label class="inputLabel" for="troubleshooterEpisode1">Select a first and second episode</label>
                                        <select is="emby-select" id="troubleshooterEpisode1" class="emby-select-withcolor emby-select"></select>
                                        <label class="inputLabel" for="troubleshooterEpisode2"> </label>
                                        <select is="emby-select" id="troubleshooterEpisode2" class="emby-select-withcolor emby-select"></select>
                                    </p>
                                </div>

                                <div id="timestampEditor" style="display: none">
                                    <br />
                                    <h3 style="margin: 0">Introduction timestamp editor</h3>
                                    <br />
                                    <h4 style="margin: 0" id="editLeftEpisodeTitle"></h4>
                                    <br />
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Recap Start</label>
                                            <input type="text" id="editLeftRecapEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftRecapEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Recap End</label>
                                            <input type="text" id="editLeftRecapEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftRecapEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                    </div>
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Intro Start</label>
                                            <input type="text" id="editLeftIntroEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftIntroEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Intro End</label>
                                            <input type="text" id="editLeftIntroEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftIntroEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                    </div>
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Credits (Outro) Start</label>
                                            <input type="text" id="editLeftCreditEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftCreditEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Credits (Outro) End</label>
                                            <input type="text" id="editLeftCreditEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftCreditEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                    </div>
                                    <div class="inlineForm">
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Preview Start</label>
                                            <input type="text" id="editLeftPreviewEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftPreviewEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                        <div class="inputContainer">
                                            <label class="inputLabel inputLabelUnfocused">Preview End</label>
                                            <input type="text" id="editLeftPreviewEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                            <input type="number" id="editLeftPreviewEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                        </div>
                                    </div>
                                    <div id="rightEpisodeEditor">
                                        <br />
                                        <h4 style="margin: 0" id="editRightEpisodeTitle"></h4>
                                        <br />
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Recap Start</label>
                                                <input type="text" id="editRightRecapEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightRecapEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Recap End</label>
                                                <input type="text" id="editRightRecapEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightRecapEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                        </div>
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Intro Start</label>
                                                <input type="text" id="editRightIntroEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightIntroEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Intro End</label>
                                                <input type="text" id="editRightIntroEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightIntroEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                        </div>
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Credits (Outro) Start</label>
                                                <input type="text" id="editRightCreditEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightCreditEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Credits (Outro) End</label>
                                                <input type="text" id="editRightCreditEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightCreditEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                        </div>
                                        <div class="inlineForm">
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Preview Start</label>
                                                <input type="text" id="editRightPreviewEpisodeStartDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightPreviewEpisodeStartEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                            <div class="inputContainer">
                                                <label class="inputLabel inputLabelUnfocused">Preview End</label>
                                                <input type="text" id="editRightPreviewEpisodeEndDisplay" class="emby-input custom-time-input" readonly />
                                                <input type="number" id="editRightPreviewEpisodeEndEdit" class="emby-input custom-time-input" style="display: none" step="any" min="0" />
                                            </div>
                                        </div>
                                    </div>
                                    <p style="margin: 20px 0">
                                        <button is="emby-button" id="btnUpdateTimestamps" class="raised button-submit block emby-button" type="button">Update timestamps</button>
                                    </p>
                                </div>
                                <p>
                                    <button is="emby-button" id="scanSeason" class="raised button-submit block emby-button" style="display: none">Scan Season</button>
                                </p>

                                <div id="timestampErrorDiv" style="display: none">
                                    <p>
                                        <textarea id="timestampError" rows="2" cols="75" readonly></textarea>
                                    </p>
                                </div>

                                <div id="fingerprintVisualizer" style="display: none">
                                    <hr style="border: none; border-top: 1px solid #ccc; margin: 20px 0" />

                                    <h3>Fingerprint Visualizer</h3>
                                    <p>
                                        Interactively compare the audio fingerprints of two episodes. <br />
                                        The blue and red bar to the right of the fingerprint diff turns blue when the corresponding fingerprint points are at least 80% similar.
                                    </p>
                                    <table>
                                        <thead>
                                            <tr>
                                                <td style="min-width: 100px; font-weight: bold">Key</td>
                                                <td style="font-weight: bold">Function</td>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr>
                                                <td>Up arrow</td>
                                                <td>Shift the left episode up by 0.1238 seconds. Holding control will shift the episode by 10 seconds.</td>
                                            </tr>
                                            <tr>
                                                <td>Down arrow</td>
                                                <td>Shift the left episode down by 0.1238 seconds. Holding control will shift the episode by 10 seconds.</td>
                                            </tr>
                                            <tr>
                                                <td>Right arrow</td>
                                                <td>Advance to the next pair of episodes.</td>
                                            </tr>
                                            <tr>
                                                <td>Left arrow</td>
                                                <td>Go back to the previous pair of episodes.</td>
                                            </tr>
                                        </tbody>
                                    </table>
                                    <br />

                                    <span>Shift amount:</span>
                                    <input type="number" min="-3000" max="3000" value="0" id="offset" />
                                    <p>
                                        <span id="suggestedShifts">
                                            <span>Suggested shifts: </span>
                                        </span>
                                    </p>
                                    <canvas id="troubleshooter" style="display: none"></canvas>
                                    <span id="timestampContainer">
                                        <span id="timestamps"></span>
                                        <br />
                                        <span id="intros"></span>
                                    </span>
                                    <br />
                                </div>

                                <div id="eraseSeasonContainer" style="display: none">
                                    <div class="checkboxContainer checkboxContainer" style="margin: 0">
                                        <label class="emby-checkbox-label">
                                            <input id="eraseSeasonCacheCheckbox" type="checkbox" is="emby-checkbox" />
                                            <span>Include cached fingerprint files</span>
                                        </label>
                                    </div>
                                    <button is="emby-button" id="btnEraseSeasonTimestamps" class="raised button-submit block emby-button" type="button">Erase all timestamps for this season</button>
                                </div>

                                <div id="eraseMovieContainer" style="display: none">
                                    <div class="checkboxContainer checkboxContainer" style="margin: 0">
                                        <label class="emby-checkbox-label">
                                            <input id="eraseMovieCacheCheckbox" type="checkbox" is="emby-checkbox" />
                                            <span>Include cached fingerprint files</span>
                                        </label>
                                    </div>
                                    <button is="emby-button" id="btnEraseMovieTimestamps" class="raised button-submit block emby-button" type="button">Erase all timestamps for this movie</button>
                                </div>

                                <hr style="border: none; border-top: 1px solid #ccc; margin: 20px 0" />

                                <div>
                                    <label class="selectLabel" for="GlobalTimestamps" style="display: none"></label>
                                    <select is="emby-select" id="GlobalTimestamps" class="emby-select-withcolor block emby-select">
                                        <option value="introduction">Global Introduction Timestamps</option>

                                        <option value="recap">Global Recap Timestamps</option>

                                        <option value="credits">Global Credits Timestamps</option>

                                        <option value="preview">Global Preview Timestamps</option>
                                    </select>
                                    <div class="checkboxContainer checkboxContainer" style="margin: 0">
                                        <label class="emby-checkbox-label">
                                            <input id="eraseModeCacheCheckbox" type="checkbox" is="emby-checkbox" />
                                            <span>Include global cached fingerprint files</span>
                                        </label>
                                    </div>
                                    <button is="emby-button" class="raised button-submit block emby-button" id="btnEraseGlobalTimestamps">Erase all selected timestamps (globally)</button>
                                </div>
                                <br />
                                <div>
                                    <p>
                                        <button is="emby-button" class="raised button-submit block emby-button" id="btnRebuildDatabase">Rebuild Local Database</button>
                                    </p>
                                    <p align="center">
                                        <b style="color: red">Rebuilding database requires a full Jellyfin restart to complete, <i>NOT</i> a dashboard restart!</b>
                                    </p>
                                </div>
                            </details>

                            <details id="support">
                                <summary>Intro Skipper Support Log</summary>

                                <textarea id="supportBundle" rows="20" cols="75" readonly></textarea>
                            </details>

                            <details id="storage">
                                <br />
                                <summary>Storage Usage</summary>
                                <div class="fieldDescription">See how much space each library uses.</div>
                                <textarea id="storageText" rows="20" cols="75" readonly></textarea>
                            </details>
                        </fieldset>
                    </form>
                </div>
            </div>

            <script src="configurationpage?name=visualizer.js"></script>

            <script>
                // Create namespace
                window.IntroSkipper = window.IntroSkipper || {};

                // Config module
                IntroSkipper.Config = (function() {
                    'use strict';

                    // Private variables
                    // first and second episodes to fingerprint & compare
                    let lhs = [];
                    let rhs = [];

                    // fingerprint point comparison & miminum similarity threshold (at most 6 bits out of 32 can be different)
                    let fprDiffs = [];
                    const fprDiffMinimum = (1 - 6 / 32) * 100;

                    // seasons grouped by show
                    let shows = {};

                // settings elements
                const visualizer = document.querySelector("details#visualizer");
                const support = document.querySelector("details#support");
                const storage = document.querySelector("details#storage");
                const btnRebuildDatabase = document.querySelector("button#btnRebuildDatabase");

                // all plugin configuration fields that can be get or set with .value (i.e. strings or numbers).
                const configurationFields = [
                    // analysis
                    "MaxParallelism",
                    "AdjustWindowInward",
                    "AdjustWindowOutward",
                    "EndSnapThreshold",
                    "ExcludeSeries",
                    "ClientList",
                    "AnalysisPercent",
                    "AnalysisLengthLimit",
                    "MinimumIntroDuration",
                    "MaximumIntroDuration",
                    "MinimumCreditsDuration",
                    "MaximumCreditsDuration",
                    "MaximumMovieCreditsDuration",
                    "MinimumRecapDuration",
                    "MaximumRecapDuration",
                    "MinimumPreviewDuration",
                    "MaximumPreviewDuration",
                    "ProcessPriority",
                    "ProcessThreads",
                    // playback
                    "IntroEndOffset",
                    "IntroStartOffset",
                    "AutoSkipDelay",
                    "SkipbuttonHideDelay",
                    // internals
                    "SilenceDetectionMaximumNoise",
                    "SilenceDetectionMinimumDuration",
                    "BlackFrameMinimumPercentage",
                    "BlackFrameThreshold",
                    "ChapterAnalyzerIntroductionPattern",
                    "ChapterAnalyzerEndCreditsPattern",
                    "ChapterAnalyzerPreviewPattern",
                    "ChapterAnalyzerRecapPattern",
                    "TypeList",
                    // UI customization
                    "AutoSkipNotificationText",
                ];

                const booleanConfigurationFields = ["AutoDetectIntros", "AnalyzeSeasonZero", "UpdateMediaSegments","UseAlternativeBlackFrameAnalyzer", "UseChapterMarkersBlackFrame", "FullLengthChapters", "RebuildMediaSegments", "ScanIntroduction", "ScanCredits", "ScanRecap", "ScanPreview", "PreferChromaprint", "CacheFingerprints", "PluginSkip", "AutoSkip", "SkipFirstEpisode", "SnapToKeyframe", "AdjustIntroBasedOnSilence", "AdjustIntroBasedOnChapters", "UseFileTransformationPlugin"];

                // visualizer elements
                const analyzerActionsSection = document.querySelector("div#analyzerActionsSection");
                const actionIntro = analyzerActionsSection.querySelector("select#actionIntro");
                const actionCredits = analyzerActionsSection.querySelector("select#actionCredits");
                const actionRecap = analyzerActionsSection.querySelector("select#actionRecap");
                const actionPreview = analyzerActionsSection.querySelector("select#actionPreview");
                const actionFileTransformation = document.getElementById("UseFileTransformationPlugin");
                const divSkipbuttonHideDelayContent = document.getElementById("divSkipbuttonHideDelayContent");
                const saveAnalyzerActionsButton = analyzerActionsSection.querySelector("button#saveAnalyzerActions");
                const scanSeasonButton = document.querySelector("button#scanSeason");
                const canvas = document.querySelector("canvas#troubleshooter");
                const selectShow = document.querySelector("select#troubleshooterShow");
                const seasonSelection = document.getElementById("seasonSelection");
                const selectSeason = document.querySelector("select#troubleshooterSeason");
                const episodeSelection = document.getElementById("episodeSelection");
                const selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
                const selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
                const txtOffset = document.querySelector("input#offset");
                const txtSuggested = document.querySelector("span#suggestedShifts");
                const btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
                const eraseSeasonContainer = document.getElementById("eraseSeasonContainer");
                const btnMovieEraseTimestamps = document.querySelector("button#btnEraseMovieTimestamps");
                const eraseMovieContainer = document.getElementById("eraseMovieContainer");
                const timestampError = document.querySelector("textarea#timestampError");
                const timestampEditor = document.querySelector("#timestampEditor");
                const rightEpisodeEditor = document.getElementById("rightEpisodeEditor");
                const btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
                const timeContainer = document.querySelector("span#timestampContainer");
                const fingerprintVisualizer = document.getElementById("fingerprintVisualizer");
                const silenceSettings = document.getElementById("silenceSettings");

                let windowHashInterval = 0;

                const pluginSkip = document.getElementById("PluginSkip");
                const serverSkipSettings = document.getElementById("ServerSkipSettings");
                const autoSkip = document.getElementById("AutoSkip");
                const autoSkipClientList = document.querySelector("div.AutoSkipClientListContainer");
                const fullLengthChapters = document.getElementById("FullLengthChapters");
                const useAlternativeBlackFrameAnalyzer = document.getElementById("UseAlternativeBlackFrameAnalyzer");
                const chapterMarkersBlackFrameSetting = document.getElementById("ChapterMarkersBlackFrameSetting");
                const snapToKeyframe = document.getElementById("SnapToKeyframe");
                const adjustIntroBasedOnSilence = document.getElementById("AdjustIntroBasedOnSilence");
                const adjustIntroBasedOnChapters = document.getElementById("AdjustIntroBasedOnChapters");
                const globalTimestamps = document.getElementById("GlobalTimestamps");
                const btnEraseGlobalTimestamps = document.getElementById("btnEraseGlobalTimestamps");

                function autoSkipChanged() {
                    if (autoSkip.checked) {
                        autoSkipClientList.style.display = "none";
                    } else {
                        autoSkipClientList.style.display = "unset";
                        autoSkipClientList.style.width = "100%";
                    }
                }

                autoSkip.addEventListener("change", autoSkipChanged);

                const recapPreviewDurations = document.getElementById("RecapPreviewDurations");

                // Hide or show durations for segments that are exclusively analyzed by chapters
                function fullLengthChaptersChanged() {
                    if (fullLengthChapters.checked) {
                        recapPreviewDurations.style.display = "none";
                    } else {
                        recapPreviewDurations.style.display = "unset";
                    }
                }

                actionFileTransformation.addEventListener("change", fileTransformationChanged);

                function fileTransformationChanged() {
                    if (actionFileTransformation.checked) {
                        divSkipbuttonHideDelayContent.style.display = "unset";
                    } else {
                        divSkipbuttonHideDelayContent.style.display = "none";
                    }
                }

                fullLengthChapters.addEventListener("change", fullLengthChaptersChanged);

                function updateList(textField, container) {
                    textField.value = Array.from(container.querySelectorAll('input[type="checkbox"]:checked'))
                        .map((checkbox) => checkbox.nextElementSibling.textContent)
                        .join(", ");
                }

                function generateCheckboxList(items, containerId, textFieldId) {
                    const container = document.getElementById(containerId);
                    const checkedItems = new Set(document.getElementById(textFieldId).value.split(", ").filter(Boolean));
                    const fragment = document.createDocumentFragment();
                    for (const item of items) {
                        const label = document.createElement("label");
                        label.className = "emby-checkbox-label";
                        label.innerHTML = '<input type="checkbox" is="emby-checkbox"' + (checkedItems.has(item) ? " checked" : "") + ">" + '<span class="checkboxLabel">' + item + "</span>";
                        fragment.appendChild(label);
                    }
                    container.innerHTML = "";
                    container.appendChild(fragment);
                    container.addEventListener(
                        "change",
                        (e) => {
                            if (e.target.type === "checkbox") updateList(document.getElementById(textFieldId), container);
                        },
                        { passive: true },
                    );
                }

                async function generateAutoSkipClientList() {
                    const response = await getJson("Devices");
                    const devices = [...new Set(response.Items.map((item) => item.AppName))];
                    generateCheckboxList(devices, "autoSkipCheckboxes", "ClientList");
                }

                async function generateAutoSkipTypeList() {
                    const types = ["Introduction", "Credits", "Recap", "Preview"];
                    generateCheckboxList(types, "autoSkipTypeCheckboxes", "TypeList");
                }

                const skipFirstEpisode = document.getElementById("divSkipFirstEpisode");
                const IntroStartOffset = document.getElementById("IntroStartOffset");
                const AutoSkipDelay = document.getElementById("AutoSkipDelay");

                async function pluginSkipSettingVisible() {
                    if (pluginSkip.checked || autoSkip.checked) {
                        pluginSkip.checked = true;
                        serverSkipSettings.style.display = "unset";
                    } else {
                        serverSkipSettings.style.display = "none";
                    }
                }

                async function alternativeBlackFrameAnalyzerSettingsVisible() {
                    if (useAlternativeBlackFrameAnalyzer.checked) {
                        chapterMarkersBlackFrameSetting.style.display = "none";
                    } else {
                        chapterMarkersBlackFrameSetting.style.display = "unset";
                    }
                }

                useAlternativeBlackFrameAnalyzer.addEventListener("change", alternativeBlackFrameAnalyzerSettingsVisible);

                async function adjustSettingsVisible() {
                    if (adjustIntroBasedOnSilence.checked) {
                        silenceSettings.style.display = "unset";
                    } else {
                        silenceSettings.style.display = "none";
                    }
                }

                adjustIntroBasedOnSilence.addEventListener("change", adjustSettingsVisible);

                async function pluginSkipSettingChanged() {
                    if (!pluginSkip.checked) {
                        autoSkip.checked = false;
                        skipFirstEpisode.checked = false;
                        IntroStartOffset.value = 0;
                    }
                    pluginSkipSettingVisible();
                }

                pluginSkip.addEventListener("change", pluginSkipSettingChanged);

                function globalTimestampChanged() {
                    btnEraseGlobalTimestamps.textContent = "Erase all " + globalTimestamps.value + " timestamps (globally)";
                }

                globalTimestamps.addEventListener("change", globalTimestampChanged);
                // https://stackoverflow.com/a/72161208/461982
                globalTimestamps.addEventListener("click", () => {
                    globalTimestamps.removeEventListener("change", globalTimestampChanged);
                    globalTimestamps.addEventListener("change", globalTimestampChanged);
                });

                // when the fingerprint visualizer opens, populate show names
                async function visualizerToggled() {
                    if (!visualizer.open) {
                        analyzerActionsSection.style.display = "none";
                        saveAnalyzerActionsButton.style.display = "none";
                        scanSeasonButton.style.display = "none";
                        return;
                    }

                    // ensure the series select is empty
                    selectShow.innerHTML = "";

                    Dashboard.showLoadingMsg();
                    shows = await getJson("Intros/Shows");

                    // Create an object to store shows by library
                    let showsByLibrary = {};

                    // Categorize shows by LibraryName
                    for (const show in shows) {
                        const libraryName = shows[show].LibraryName || "Uncategorized";
                        if (!showsByLibrary[libraryName]) {
                            showsByLibrary[libraryName] = [];
                        }
                        showsByLibrary[libraryName].push({
                            value: show,
                            text: shows[show].SeriesName + " (" + shows[show].ProductionYear + ")",
                        });
                    }

                    // Add categorized shows to the select element
                    for (const library in showsByLibrary) {
                        const optgroup = document.createElement("optgroup");
                        optgroup.label = library;

                        showsByLibrary[library].forEach((show) => {
                            const option = document.createElement("option");
                            option.value = show.value;
                            option.textContent = show.text;
                            optgroup.appendChild(option);
                        });

                        selectShow.appendChild(optgroup);
                    }

                    selectShow.value = "";
                    Dashboard.hideLoadingMsg();
                }

                // fetch the support bundle whenever the detail section is opened.
                async function supportToggled() {
                    if (!support.open) {
                        return;
                    }

                    // Fetch the support bundle
                    const bundle = await fetchWithAuth("IntroSkipper/SupportBundle", "GET", null);
                    const bundleText = await bundle.text();

                    // Display it to the user and select all
                    const ta = document.querySelector("textarea#supportBundle");
                    ta.value = bundleText;
                    ta.focus();
                    ta.setSelectionRange(0, ta.value.length);

                    // Attempt to copy it to the clipboard automatically, falling back
                    // to prompting the user to press Ctrl + C.
                    try {
                        navigator.clipboard.writeText(bundleText);
                        Dashboard.alert("Support bundle copied to clipboard");
                    } catch {
                        Dashboard.alert("Press Ctrl+C to copy support bundle");
                    }
                }

                // fetch the storage whenever the detail section is opened.
                async function storageToggled() {
                    if (!storage.open) {
                        return;
                    }

                    // Fetch the support bundle
                    const bundle = await fetchWithAuth("IntroSkipper/Storage", "GET", null);
                    const bundleText = await bundle.text();

                    // Display it to the user
                    const ta = document.querySelector("textarea#storageText");
                    ta.value = bundleText;
                }

                // show changed, populate seasons
                async function showChanged() {
                    seasonSelection.style.display = "unset";
                    clearSelect(selectSeason);
                    eraseSeasonContainer.style.display = "none";
                    eraseMovieContainer.style.display = "none";
                    episodeSelection.style.display = "unset";
                    clearSelect(selectEpisode1);
                    clearSelect(selectEpisode2);

                    if (shows[selectShow.value].IsMovie) {
                        await movieLoaded();
                        return;
                    }

                    // add all seasons from this show to the season select
                    for (const season in shows[selectShow.value].Seasons) {
                        addItem(selectSeason, "Season " + shows[selectShow.value].Seasons[season], season);
                    }

                    selectSeason.value = "";
                }

                // season changed, reload all episodes
                async function seasonChanged() {
                    const seasonData = encodeURI(selectShow.value) + "/" + encodeURI(selectSeason.value);
                    Dashboard.showLoadingMsg();
                    // show the analyzer actions editor.
                    saveAnalyzerActionsButton.style.display = "block";
                    saveAnalyzerActionsButton.textContent = "Apply to season";
                    scanSeasonButton.style.display = "block";
                    scanSeasonButton.textContent = "Scan season";
                    const analyzerActions = await getJson("Intros/AnalyzerActions/" + encodeURI(selectSeason.value));
                    actionIntro.value = analyzerActions.Introduction || "Default";
                    actionCredits.value = analyzerActions.Credits || "Default";
                    actionRecap.value = analyzerActions.Recap || "Default";
                    actionPreview.value = analyzerActions.Preview || "Default";
                    analyzerActionsSection.style.display = "unset";

                    // show the erase season button
                    eraseSeasonContainer.style.display = "unset";

                    clearSelect(selectEpisode1);
                    clearSelect(selectEpisode2);
                    let i = 1;
                    const episodes = await getJson("Intros/Show/" + seasonData);
                    for (const episode in episodes) {
                        const strI = i.toLocaleString("en", {
                            minimumIntegerDigits: 2,
                            maximumFractionDigits: 0,
                        });
                        addItem(selectEpisode1, strI + ": " + episodes[episode].Name, episodes[episode].Id);
                        addItem(selectEpisode2, strI + ": " + episodes[episode].Name, episodes[episode].Id);
                        i++;
                    }
                    Dashboard.hideLoadingMsg();

                    setTimeout(() => {
                        selectEpisode1.selectedIndex = 0;
                        selectEpisode2.selectedIndex = 1;
                        episodeChanged();
                    }, 100);
                }

                // episode changed, get fingerprints & calculate diff
                async function episodeChanged() {
                    if (!selectEpisode1.value || !selectEpisode2.value) {
                        return;
                    }

                    Dashboard.showLoadingMsg();

                    timestampError.value = "";
                    fingerprintVisualizer.style.display = "unset";
                    canvas.style.display = "none";

                    lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
                    if (lhs === undefined) {
                        timestampError.value += "Error: " + selectEpisode1.value + " fingerprints failed!\n";
                    } else if (lhs === null) {
                        timestampError.value += selectEpisode1.value + " fingerprints missing or incomplete.\n";
                    }

                    rightEpisodeEditor.style.display = "unset";

                    rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");
                    if (rhs === undefined) {
                        timestampError.value += "Error: " + selectEpisode2.value + " fingerprints failed!";
                    } else if (rhs === null) {
                        timestampError.value += selectEpisode2.value + " fingerprints missing or incomplete.\n";
                    }

                    if (timestampError.value === "") {
                        timestampErrorDiv.style.display = "none";
                    } else {
                        timestampErrorDiv.style.display = "unset";
                    }

                    Dashboard.hideLoadingMsg();

                    txtOffset.value = "0";
                    IntroSkipper.Visualizer.refreshBounds();
                    IntroSkipper.Visualizer.renderTroubleshooter();
                    IntroSkipper.Visualizer.findExactMatches();
                    await updateTimestampEditor();
                }

                async function movieLoaded() {
                    Dashboard.showLoadingMsg();

                    saveAnalyzerActionsButton.textContent = "Apply to movie";
                    scanSeasonButton.textContent = "Scan movie";
                    seasonSelection.style.display = "none";
                    episodeSelection.style.display = "none";
                    eraseMovieContainer.style.display = "unset";
                    scanSeasonButton.style.display = "block";

                    timestampError.value = "";
                    fingerprintVisualizer.style.display = "none";
                    rightEpisodeEditor.style.display = "none";

                    if (timestampError.value === "") {
                        timestampErrorDiv.style.display = "none";
                    } else {
                        timestampErrorDiv.style.display = "unset";
                    }

                    Dashboard.hideLoadingMsg();

                    txtOffset.value = "0";

                    await updateTimestampEditor();
                }

                function setupTimeInputs() {
                    const timestampEditor = document.getElementById("timestampEditor");
                    timestampEditor.querySelectorAll(".inputContainer").forEach((container) => {
                        const displayInput = container.querySelector('[id$="Display"]');
                        const editInput = container.querySelector('[id$="Edit"]');
                        displayInput.addEventListener("pointerdown", (e) => {
                            e.preventDefault();
                            switchToEdit(displayInput, editInput);
                        });
                        editInput.addEventListener("blur", () => switchToDisplay(displayInput, editInput));
                        displayInput.value = formatTime(parseFloat(editInput.value) || 0);
                    });
                }

                function switchToEdit(displayInput, editInput) {
                    displayInput.style.display = "none";
                    editInput.style.display = "";
                    editInput.focus();
                }

                function switchToDisplay(displayInput, editInput) {
                    editInput.style.display = "none";
                    displayInput.style.display = "";
                    displayInput.value = formatTime(parseFloat(editInput.value) || 0);
                }

                function formatTime(totalSeconds) {
                    const hours = Math.floor(totalSeconds / 3600);
                    const minutes = Math.floor((totalSeconds % 3600) / 60);
                    const seconds = Math.floor(totalSeconds % 60);
                    let result = [];
                    if (hours > 0) result.push(hours + " hour" + (hours !== 1 ? "s" : ""));
                    if (minutes > 0) result.push(minutes + " minute" + (minutes !== 1 ? "s" : ""));
                    if (seconds > 0 || result.length === 0) result.push(seconds + " second" + (seconds !== 1 ? "s" : ""));
                    return result.join(" ");
                }

                // updates the timestamp editor
                async function updateTimestampEditor() {
                    // Get the title and ID of the left and right episodes
                    const leftEpisode = selectEpisode1.options[selectEpisode1.selectedIndex] || selectShow;

                    // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
                    const leftEpisodeJson = await getJson("Episode/" + leftEpisode.value + "/Timestamps");

                    // Update the editor for the first episode
                    timestampEditor.style.display = "unset";
                    document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
                    document.querySelector("#editLeftIntroEpisodeStartEdit").value = leftEpisodeJson.Introduction.Start;
                    document.querySelector("#editLeftIntroEpisodeEndEdit").value = leftEpisodeJson.Introduction.End;
                    document.querySelector("#editLeftCreditEpisodeStartEdit").value = leftEpisodeJson.Credits.Start;
                    document.querySelector("#editLeftCreditEpisodeEndEdit").value = leftEpisodeJson.Credits.End;
                    document.querySelector("#editLeftRecapEpisodeStartEdit").value = leftEpisodeJson.Recap.Start;
                    document.querySelector("#editLeftRecapEpisodeEndEdit").value = leftEpisodeJson.Recap.End;
                    document.querySelector("#editLeftPreviewEpisodeStartEdit").value = leftEpisodeJson.Preview.Start;
                    document.querySelector("#editLeftPreviewEpisodeEndEdit").value = leftEpisodeJson.Preview.End;

                    // Update the editor for the second episode
                    if (rightEpisodeEditor.style.display !== "none") {
                        const rightEpisode = selectEpisode2.options[selectEpisode2.selectedIndex];

                        // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
                        const rightEpisodeJson = await getJson("Episode/" + rightEpisode.value + "/Timestamps");

                        // Update the editor for the second episode
                        document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
                        document.querySelector("#editRightIntroEpisodeStartEdit").value = rightEpisodeJson.Introduction.Start;
                        document.querySelector("#editRightIntroEpisodeEndEdit").value = rightEpisodeJson.Introduction.End;
                        document.querySelector("#editRightCreditEpisodeStartEdit").value = rightEpisodeJson.Credits.Start;
                        document.querySelector("#editRightCreditEpisodeEndEdit").value = rightEpisodeJson.Credits.End;
                        document.querySelector("#editRightRecapEpisodeStartEdit").value = rightEpisodeJson.Recap.Start;
                        document.querySelector("#editRightRecapEpisodeEndEdit").value = rightEpisodeJson.Recap.End;
                        document.querySelector("#editRightPreviewEpisodeStartEdit").value = rightEpisodeJson.Preview.Start;
                        document.querySelector("#editRightPreviewEpisodeEndEdit").value = rightEpisodeJson.Preview.End;
                    }

                    // Update display inputs
                    const inputs = document.querySelectorAll('#timestampEditor input[type="number"]');
                    inputs.forEach((input) => {
                        const displayInput = document.getElementById(input.id.replace("Edit", "Display"));
                        displayInput.value = formatTime(parseFloat(input.value) || 0);
                    });

                    setupTimeInputs();
                }

                // adds an item to a dropdown
                function addItem(select, text, value) {
                    let item = new Option(text, value);
                    select.add(item);
                }

                // clear selection of items
                function clearSelect(select) {
                    timestampError.value = "";
                    timestampErrorDiv.style.display = "none";
                    timestampEditor.style.display = "none";
                    timeContainer.style.display = "none";
                    canvas.style.display = "none";
                    let i,
                        L = select.options.length - 1;
                    for (i = L; i >= 0; i--) {
                        select.remove(i);
                    }
                }

                // make an authenticated GET to the server and parse the response as JSON
                async function getJson(url) {
                    return await fetchWithAuth(url, "GET")
                        .then((r) => {
                            if (r.ok) {
                                return r.json();
                            } else {
                                return null;
                            }
                        })
                        .catch((err) => {
                            console.debug(err);
                        });
                }

                // make an authenticated fetch to the server
                async function fetchWithAuth(url, method, body) {
                    url = ApiClient.serverAddress() + "/" + url;

                    const reqInit = {
                        method: method,
                        headers: {
                            Authorization: "MediaBrowser Token=" + ApiClient.accessToken(),
                        },
                        body: body,
                    };

                    if (method === "POST") {
                        reqInit.headers["Content-Type"] = "application/json";
                    }

                    return await fetch(url, reqInit);
                }

                // key pressed
                function keyDown(e) {
                    let episodeDelta = 0;
                    let offsetDelta = 0;

                    switch (e.key) {
                        case "ArrowDown":
                            if (timestampError.value !== "") {
                                // if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
                                offsetDelta = e.ctrlKey ? 10 / 0.1238 : 1;
                            }
                            break;

                        case "ArrowUp":
                            if (timestampError.value !== "") {
                                offsetDelta = e.ctrlKey ? -10 / 0.1238 : -1;
                            }
                            break;

                        case "ArrowRight":
                            episodeDelta = 2;
                            break;

                        case "ArrowLeft":
                            episodeDelta = -2;
                            break;

                        default:
                            return;
                    }

                    if (offsetDelta !== 0) {
                        txtOffset.value = Number(txtOffset.value) + Math.floor(offsetDelta);
                    }

                    if (episodeDelta !== 0) {
                        // calculate the number of episodes remaining in the LHS and RHS episode pickers
                        const lhsRemaining = selectEpisode1.selectedIndex;
                        const rhsRemaining = selectEpisode2.length - selectEpisode2.selectedIndex - 1;

                        // if we're moving forward and the right episode picker is close to the end, don't move.
                        if (episodeDelta > 0 && rhsRemaining <= 1) {
                            return;
                        } else if (episodeDelta < 0 && lhsRemaining <= 1) {
                            return;
                        }

                        selectEpisode1.selectedIndex += episodeDelta;
                        selectEpisode2.selectedIndex += episodeDelta;
                        episodeChanged();
                    }

                    IntroSkipper.Visualizer.renderTroubleshooter();
                    e.preventDefault();
                }

                // check that the user is still on the configuration page
                function checkWindowHash() {
                    const h = location.hash;
                    if (h === "#!/configurationpage?name=Intro%20Skipper" || h.includes("#!/dialog")) {
                        return;
                    }

                    console.debug("navigated away from intro skipper configuration page");
                    document.removeEventListener("keydown", keyDown);
                    clearInterval(windowHashInterval);
                }

                // converts seconds to a readable timestamp (i.e. 127 becomes "02:07").
                function secondsToString(seconds) {
                    return new Date(seconds * 1000).toISOString().slice(14, 19);
                }

                // Check if File Transformation plugin is available and enable/disable skip button settings
                function checkFileTransformationPlugin(config) {
                    const skipButtonField = document.querySelector("#SkipbuttonHideDelay");
                    const skipButtonContainer = document.querySelector("#divSkipbuttonHideDelay");
                    const warning = document.querySelector("#fileTransformationWarning");

                    if (config.FileTransformationPluginEnabled) {
                        // Plugin is available - enable the field
                        skipButtonField.disabled = false;
                        skipButtonContainer.classList.remove("disabled-block");
                        warning.style.display = "none";
                    } else {
                        // Plugin is not available - disable the field and show warning
                        skipButtonField.disabled = true;
                        actionFileTransformation.disabled = true;
                        actionFileTransformation.checked = false;
                        skipButtonContainer.classList.add("disabled-block");
                        warning.style.display = "block";
                    }
                }

                // erase all intro/credits timestamps
                function eraseTimestamps(mode) {
                    const lower = mode.toLocaleLowerCase();
                    const title = "Confirm timestamp erasure";
                    const body = "Are you sure you want to erase all previously discovered " + mode.toLocaleLowerCase() + " timestamps?";
                    const eraseCacheChecked = document.getElementById("eraseModeCacheCheckbox").checked;

                    Dashboard.confirm(body, title, (result) => {
                        if (!result) {
                            return;
                        }

                        fetchWithAuth("Intros/EraseTimestamps?mode=" + mode + "&eraseCache=" + eraseCacheChecked, "POST", null);

                        Dashboard.alert(mode + " timestamps erased");
                        document.getElementById("eraseModeCacheCheckbox").checked = false;
                    });
                }

                function loadConfiguration() {
                    Dashboard.showLoadingMsg();
                    ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b")
                        .then((config) => {
                            for (const field of configurationFields) {
                                document.querySelector("#" + field).value = config[field];
                            }

                            for (const field of booleanConfigurationFields) {
                                document.querySelector("#" + field).checked = config[field];
                            }

                            fullLengthChaptersChanged();
                            autoSkipChanged();
                            generateAutoSkipTypeList();
                            generateAutoSkipClientList();
                            pluginSkipSettingVisible();
                            fileTransformationChanged();
                            alternativeBlackFrameAnalyzerSettingsVisible();
                            adjustSettingsVisible();
                            globalTimestampChanged();
                            checkFileTransformationPlugin(config);

                            Dashboard.hideLoadingMsg();
                        })
                        .catch((error) => {
                            Dashboard.hideLoadingMsg();
                        });
                }

                // Initial load
                document.querySelector("#TemplateConfigPage").addEventListener("pageshow", loadConfiguration);

                document.querySelector("#FingerprintConfigForm").addEventListener("submit", (e) => {
                    Dashboard.showLoadingMsg();
                    ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b")
                        .then((config) => {
                            for (const field of configurationFields) {
                                config[field] = document.querySelector("#" + field).value;
                            }

                            for (const field of booleanConfigurationFields) {
                                config[field] = document.querySelector("#" + field).checked;
                            }

                            ApiClient.updatePluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b", config)
                                .then((result) => {
                                    Dashboard.hideLoadingMsg();
                                    Dashboard.processPluginConfigurationUpdateResult(result);
                                })
                                .catch((error) => {
                                    Dashboard.hideLoadingMsg();
                                });
                        })
                        .catch((error) => {
                            Dashboard.hideLoadingMsg();
                        });

                    e.preventDefault();
                    return false;
                });

                visualizer.addEventListener("toggle", visualizerToggled);
                support.addEventListener("toggle", supportToggled);
                storage.addEventListener("toggle", storageToggled);
                txtOffset.addEventListener("change", function() {
                    IntroSkipper.Visualizer.renderTroubleshooter();
                });
                selectShow.addEventListener("change", showChanged);
                selectSeason.addEventListener("change", seasonChanged);
                selectEpisode1.addEventListener("change", episodeChanged);
                selectEpisode2.addEventListener("change", episodeChanged);

                saveAnalyzerActionsButton.addEventListener("click", () => {
                    Dashboard.showLoadingMsg();

                    const url = "Intros/AnalyzerActions/UpdateSeason";
                    const actions = {
                        id: selectSeason.value,
                        analyzerActions: {
                            Introduction: actionIntro.value,
                            Credits: actionCredits.value,
                            Recap: actionRecap.value,
                            Preview: actionPreview.value,
                        },
                    };

                    fetchWithAuth(url, "POST", JSON.stringify(actions));

                    Dashboard.alert("Analyzer actions updated for " + selectSeason.value + " of " + selectShow.value);
                    Dashboard.hideLoadingMsg();
                });

                scanSeasonButton.addEventListener("click", async () => {
                    Dashboard.showLoadingMsg();
                    console.log("(Re-) Scan task started.");
                    await fetchWithAuth("Intros/ScanSeason/" + selectShow.value + "/" + (selectSeason.value || selectShow.value), "POST", null);
                    await updateTimestampEditor();
                    Dashboard.hideLoadingMsg();
                });

                btnUpdateTimestamps.addEventListener("click", () => {
                    const getEditValue = (id) => parseFloat(document.getElementById(id).value) || 0;

                    // Check if we're dealing with a movie or TV show
                    const isMovie = selectShow.value && shows[selectShow.value] && shows[selectShow.value].IsMovie;

                    let lhsId;
                    if (isMovie) {
                        // For movies, use the show ID directly
                        lhsId = selectShow.value;
                    } else {
                        // For TV shows, use the selected episode
                        const selectedOption = selectEpisode1.options[selectEpisode1.selectedIndex];
                        if (!selectedOption) {
                            Dashboard.alert("Please select an episode first");
                            return;
                        }
                        lhsId = selectedOption.value;
                    }

                    const newLhs = {
                        Introduction: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftIntroEpisodeStartEdit"),
                            End: getEditValue("editLeftIntroEpisodeEndEdit"),
                        },
                        Credits: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftCreditEpisodeStartEdit"),
                            End: getEditValue("editLeftCreditEpisodeEndEdit"),
                        },
                        Recap: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftRecapEpisodeStartEdit"),
                            End: getEditValue("editLeftRecapEpisodeEndEdit"),
                        },
                        Preview: {
                            ItemId: lhsId,
                            Start: getEditValue("editLeftPreviewEpisodeStartEdit"),
                            End: getEditValue("editLeftPreviewEpisodeEndEdit"),
                        },
                    };

                    // Save the first/left timestamps
                    fetchWithAuth("Episode/" + lhsId + "/Timestamps", "POST", JSON.stringify(newLhs));

                    // For TV shows, also save the second episode's timestamps
                    if (!isMovie && rightEpisodeEditor.style.display !== "none") {
                        const rhsSelectedOption = selectEpisode2.options[selectEpisode2.selectedIndex];
                        if (rhsSelectedOption) {
                            const rhsId = rhsSelectedOption.value;
                            const newRhs = {
                                Introduction: {
                                    ItemId: rhsId,
                                    Start: getEditValue("editRightIntroEpisodeStartEdit"),
                                    End: getEditValue("editRightIntroEpisodeEndEdit"),
                                },
                                Credits: {
                                    ItemId: rhsId,
                                    Start: getEditValue("editRightCreditEpisodeStartEdit"),
                                    End: getEditValue("editRightCreditEpisodeEndEdit"),
                                },
                                Recap: {
                                    ItemId: rhsId,
                                    Start: getEditValue("editRightRecapEpisodeStartEdit"),
                                    End: getEditValue("editRightRecapEpisodeEndEdit"),
                                },
                                Preview: {
                                    ItemId: rhsId,
                                    Start: getEditValue("editRightPreviewEpisodeStartEdit"),
                                    End: getEditValue("editRightPreviewEpisodeEndEdit"),
                                },
                            };
                            fetchWithAuth("Episode/" + rhsId + "/Timestamps", "POST", JSON.stringify(newRhs));
                        }
                    }

                    Dashboard.alert("New segment timestamps saved");
                });

                btnSeasonEraseTimestamps.addEventListener("click", () => {
                    Dashboard.confirm("Are you sure you want to erase all timestamps for this season?", "Confirm timestamp erasure", (result) => {
                        if (!result) {
                            return;
                        }

                        const show = selectShow.value;
                        const season = selectSeason.value;
                        const eraseCacheChecked = document.getElementById("eraseSeasonCacheCheckbox").checked;

                        const url = "Intros/Show/" + encodeURIComponent(show) + "/" + encodeURIComponent(season);
                        fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null);

                        Dashboard.alert("Erased timestamps for " + season + " of " + show);
                        document.getElementById("eraseSeasonCacheCheckbox").checked = false;
                    });
                });

                btnMovieEraseTimestamps.addEventListener("click", () => {
                    Dashboard.confirm("Are you sure you want to erase all timestamps for this movie?", "Confirm timestamp erasure", (result) => {
                        if (!result) {
                            return;
                        }

                        const show = selectShow.value;
                        const eraseCacheChecked = document.getElementById("eraseMovieCacheCheckbox").checked;

                        const url = "Intros/Show/" + encodeURIComponent(show);
                        fetchWithAuth(url + "?eraseCache=" + eraseCacheChecked, "DELETE", null);

                        Dashboard.alert("Erased timestamps for " + show);
                        document.getElementById("eraseMovieCacheCheckbox").checked = false;
                    });
                });

                btnEraseGlobalTimestamps.addEventListener("click", (e) => {
                    switch (globalTimestamps.value) {
                        case "introduction":
                            eraseTimestamps("Introduction");
                            break;
                        case "recap":
                            eraseTimestamps("Recap");
                            break;
                        case "credits":
                            eraseTimestamps("Credits");
                            break;
                        case "preview":
                            eraseTimestamps("Preview");
                            break;
                        default:
                            return;
                    }
                    e.preventDefault();
                });

                btnRebuildDatabase.addEventListener("click", () => {
                    fetchWithAuth("Intros/RebuildDatabase", "POST", null);
                });

                document.addEventListener("keydown", keyDown);
                windowHashInterval = setInterval(checkWindowHash, 2500);

                canvas.addEventListener("mousemove", (e) => {
                    const rect = e.currentTarget.getBoundingClientRect();
                    const y = e.clientY - rect.top;
                    const shift = Number(txtOffset.value);

                    let lTime, rTime, diffPos;
                    if (shift < 0) {
                        lTime = y * 0.1238;
                        rTime = (y + shift) * 0.1238;
                        diffPos = y + shift;
                    } else {
                        lTime = (y - shift) * 0.1238;
                        rTime = y * 0.1238;
                        diffPos = y - shift;
                    }

                    const diff = fprDiffs[Math.floor(diffPos)];

                    if (!diff) {
                        timeContainer.style.display = "none";
                        return;
                    } else {
                        timeContainer.style.display = "unset";
                    }

                    const times = document.querySelector("span#timestamps");

                    // LHS timestamp, RHS timestamp, percent similarity
                    times.textContent = secondsToString(lTime) + ", " + secondsToString(rTime) + ", " + Math.round(diff) + "%";

                    timeContainer.style.position = "relative";
                    timeContainer.style.left = "25px";
                    timeContainer.style.top = (-1 * rect.height + y).toString() + "px";
                });

                function setTime(seconds) {
                    // Calculate hours, minutes, and remaining seconds
                    let hours = Math.floor(seconds / 3600);
                    let minutes = Math.floor((seconds % 3600) / 60);
                    let remainingSeconds = seconds % 60;

                    // Format as HH:MM:SS and set the value of the time input
                    return String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(remainingSeconds).padStart(2, "0");
                }

                function getTimeInSeconds(time) {
                    let [hours, minutes, seconds] = time.split(":").map(Number);
                    return hours * 3600 + minutes * 60 + seconds;
                }

                // Public API
                return {
                    // Expose state for visualizer
                    getState: function() {
                        return {
                            lhs: lhs,
                            rhs: rhs,
                            fprDiffs: fprDiffs,
                            fprDiffMinimum: fprDiffMinimum
                        };
                    },

                    // Expose DOM elements
                    getElements: function() {
                        return {
                            canvas: canvas,
                            txtOffset: txtOffset,
                            selectEpisode1: selectEpisode1,
                            selectEpisode2: selectEpisode2,
                            txtSuggested: txtSuggested,
                            timeContainer: timeContainer
                        };
                    },

                    // Expose utility functions
                    utils: {
                        secondsToString: secondsToString
                    },

                    // Setter for visualizer to update state
                    setFprDiffs: function(diffs) {
                        fprDiffs = diffs;
                    }
                };
                })();
            </script>
        </div>
    </body>
</html>
